<?php
/**
 * Templum - php template engine.
 *
 * Features:
 *  - automatically create cache
 *  - automatically create helper objects if need
 *  - assign vars in natural form. Example: $oTemplum->myvar[0]['name'] = 'Jonny Five';
 *
 * @link http://templum.googlecode.com
 * @license Apache License 2.0 http://www.apache.org/licenses/LICENSE-2.0
 * @author Anton Danilchenko <anton.danilchenko@gmail.com>
 */
final class Templum {
	/**
	 * Helpers objects.
	 *
	 * @var array
	 */
	private $_aHelpers = array();
	/**
	 * Data to processing in helper.
	 *
	 * @var string
	 */
	private $_sHelperData = '';
	/**
	 * Path to helpers directory.
	 *
	 * @var string
	 */
	private $_sHelpersDir;
	/**
	 * Path to templates directory.
	 *
	 * @var string
	 */
	private $_sTemplateDir;
	/**
	 * Path to cache directory.
	 *
	 * @var string
	 */
	private $_sCacheDir;


	/**
	 * Set directory paths.
	 *
	 * @param string $sTemplateDir: Path to templates directory
	 * @param string $sCacheDir: Path to cache directory
	 * @return Templum
	 */
	public function __construct($sTemplateDir='template', $sCacheDir='cache') {
		// set date (need for save cache file)
		if (! date_default_timezone_get())
			date_default_timezone_set('Europe/Kiev');
		// set helpers directory
		$sHelpersDir = dirname(__FILE__) . '/Helper';
		// check directory
		if ( ! is_dir($sHelpersDir) OR ! is_readable($sHelpersDir) )
			throw new Templum_Exception("Helpers directory '{$sHelpersDir}' not found or not readable");
		if ( ! is_dir($sTemplateDir) OR ! is_readable($sTemplateDir) )
			throw new Templum_Exception("Template directory '{$sTemplateDir}' should be created and readable");
		if ( ! is_dir($sCacheDir) OR ! is_readable($sCacheDir) OR ! is_writable($sCacheDir))
			throw new Templum_Exception("Cache directory '{$sCacheDir}' should be created, readable and writable");
		// set directory
		$this->_sTemplateDir = $sTemplateDir;
		$this->_sCacheDir = $sCacheDir;
		$this->_sHelpersDir = $sHelpersDir;
	}


	/**
	 * Check template file.
	 *
	 * @param string $sPath: Path to template file (start from template dir)
	 * @var string
	 */
	public function hasExist($sPath) {
		// get full path to template
		$sTemplatePath = $this->_getTemplatePath($sPath);
		// check file on disk
		return file_exists($sTemplatePath);
	}


	/**
	 * Execute and return template.
	 *
	 * @param string $sPath: Path to template file (start from template dir)
	 * @var string
	 */
	public function getTemplate($sPath) {
		// renew cache
		if ( ! $this->_isCached($sPath) ) {
			// get full path to template
			$sTemplatePath = $this->_getTemplatePath($sPath);
			// check file
			if (! file_exists($sTemplatePath))
				throw new Templum_Exception("Template file '{$sTemplatePath}' not found");
			// read template file
			$sTemplateSource = implode('', file($sTemplatePath));
			// replace placeholders
			$sTemplateData = $this->_getTemplatePrepared($sTemplateSource);
			// save changed template data to cache
			$this->_setCache($sPath, $sTemplateData);
		}
		// collect printed data in buffer
		ob_start();
		// execute cache
		$this->_getCache($sPath);
		// save collected data in variable
		$sProcessedTemplate = ob_get_contents();
		// stop collect data
		ob_end_clean();
		// return collected data
		return $sProcessedTemplate;
	}


	/**
	 * Clear all cache files.
	 *
	 * @return bool
	 */
	public function clearCache() {
		self::_rmDir($this->_sCacheDir, true, false);
	}


	/**
	 * Get helper object (without parameters).
	 *
	 * @param string $sName: Helper name
	 */
	public function __get($sName) {
		return $this->__call($sName, array());
	}


	/**
	 * Get helper object (with parameters).
	 *
	 * @param string $sName: Helper name
	 * @param string $aParams: Helper parameters
	 */
	public function __call($sName, $aParams) {
		// correct helper name
		$sName = ucfirst(strtolower($sName));
		// create new helper
		if (! isset($this->_aHelpers[$sName]) ) {
			// set path to helper file
			$sHelperFile = $this->_getHelperPath("{$sName}.php");
			// check helper file
			if (! file_exists($sHelperFile) )
				throw new Templum_Exception("Helper with name '{$sName}' not defined");
			// include file
			require_once ("{$this->_sHelpersDir}/Interface.php");
			require_once $sHelperFile;
			// define helren full name
			$sHelperName = "Templum_Helper_{$sName}";
			// check file interface
			if ( ! class_exists($sHelperName) OR ! in_array('Templum_Helper_Interface', class_implements($sHelperName)) )
				throw new Templum_Exception("Helper with name '{$sHelperName}' not defined or not implement of interface Templum_Helper_Interface");
			// create helper
			$this->_aHelpers[$sName] = new $sHelperName;
		}
		// processing data in helper
		$this->_aHelpers[$sName]->handle($this->_sHelperData, $aParams);
		// return itself (for OOP calls)
		return $this;
	}


	/**
	 * Print helper handled data.
	 *
	 * @return string
	 */
	public function __toString() {
		return (string)$this->_sHelperData;
	}


	/**
	 * Check if cache actual.
	 *
	 * @param string $sPath: Path to template file (start from template dir)
	 * @return bool
	 */
	private function _isCached($sPath) {
		// full path to template and cache files
		$sCachePath = $this->_getCachePath($sPath);
		$sTemplatePath = $this->_getTemplatePath($sPath);
		// cache newest
		if (file_exists($sTemplatePath) AND file_exists($sCachePath) AND filemtime($sTemplatePath) <= filemtime($sCachePath))
			return true;
		// cache old
		return false;
	}



	/**
	 * Return cache data.
	 *
	 * @param string $sPath: Path to template file (start from template dir)
	 * @return string
	 */
	private function _getCache($sPath) {
		// full path to template
		$sCachePath = $this->_getCachePath($sPath);
		// check template in cache
		if(! $this->_isCached($sPath))
			throw new Templum_Exception("Cache file not found. Path: {$sCachePath}");
		// insert variables to view in template
		extract(get_object_vars($this));
		// return cache data
		return (include $sCachePath);
	}


	/**
	 * Save cache file.
	 *
	 * @param string $sPath: Path to template file (start from template dir)
	 * @param string $sData: Saved data to cache
	 * @return bool
	 */
	private function _setCache($sPath, $sData) {
		// full path to template
		$sCachePath = $this->_getCachePath($sPath);
		$sTemplatePath = $this->_getTemplatePath($sPath);
		// create directory hierarhy
		$sDirs = dirname($sCachePath);
		if (! is_dir($sDirs))
			mkdir($sDirs, 0755, true);
		// save cache
		$oFile = fopen($sCachePath, 'w');
		fwrite($oFile, $sData);
		fclose($oFile);
		// set cache modification time
		$iDate = filemtime($sTemplatePath);
		touch($sCachePath, $iDate);
	}


	/**
	 * Return full path to template.
	 *
	 * @param string $sPath: Path to template file (start from template dir)
	 */
	private function _getTemplatePath($sPath) {
		// delete start slash (or backslash)
		$sPath = ltrim($sPath, '/\\');
		// full path to template file
		return "{$this->_sTemplateDir}/{$sPath}";
	}


	/**
	 * Return full path to cache file.
	 *
	 * @param string $sPath: Path to template file (start from template dir)
	 */
	private function _getCachePath($sPath) {
		// delete start slash (or backslash)
		$sPath = ltrim($sPath, '/\\');
		// full path to cache file
		return "{$this->_sCacheDir}/{$sPath}";
	}


	/**
	 * Return full path to cache file.
	 *
	 * @param string $sPath: Path to helper file (start from helpers dir)
	 */
	private function _getHelperPath($sPath) {
		// delete start slash (or backslash)
		$sPath = ltrim($sPath, '/\\');
		// full path to cache file
		return "{$this->_sHelpersDir}/{$sPath}";
	}


	/**
	 * Replace placeholders in template and return result.
	 *
	 * @param string $sTemplateSource: Raw template code
	 */
	private function _getTemplatePrepared($sTemplateSource) {
		// check template syntax
		if (preg_match('`(<\?|\?>)`', $sTemplateSource))
			throw new Templum_Exception('In template code should not be php tags (<?, ?>)');
		// tag prefix an suffix
		$sTagPrefix = '\{ *';
		$sTagSuffix = ' *\}';
		$sTagVariable = '\$.+';
		$sTagInclude = '[\'"]([^; ]+)[\'"]';
		$sTagCondition = '\( *(.*) *\)';
		$sTagConditionForeach = '\( *([^ ]+) .+\)';
		// set expressions
		$aExpressionsShort = array(
			"(if *{$sTagCondition})"				=>	'<?php $1 { ?>',
			"(elseif *{$sTagCondition})"			=>	'<?php } $1 { ?>',
			"(else)"								=>	'<?php } $1 { ?>',
			"(foreach *{$sTagConditionForeach})"	=>	'<?php if(!empty($2)) $1 { ?>',
			"/(if|foreach)"							=>	'<?php } ?>',
			"({$sTagVariable})"						=>	'<?php echo ($1); ?>',
			"include {$sTagInclude}"				=>	'<?php echo $this->getTemplate("$1"); ?>',
			"helper *\( *$.+\)(.*)"					=>	'<?php if (isset($1)) { $this->_setHelperData($1); } else { $this->_setHelperData(); } echo \$this$2; ?>',
			"helper *{$sTagCondition}(.*)"			=>	'<?php $this->_setHelperData($1); echo \$this$2; ?>',
		);
		// prepare all expressions
		$aExpressions = array();
		foreach ($aExpressionsShort as $sKey=>&$sValue) {
			$sKey = "`{$sTagPrefix}{$sKey}{$sTagSuffix}`U";
			$aExpressions[$sKey] = $sValue;
		}
		// processing template
		$sTemplateSource = preg_replace(array_keys($aExpressions), array_values($aExpressions), $sTemplateSource);
		// return result;
		return $sTemplateSource;
	}


	/**
	 * Set data to processing in helpers.
	 *
	 * @param string $sText: Text to processing in helper
	 */
	private function _setHelperData($sText='') {
		$this->_sHelperData = $sText;
	}


	/**
	 * Delete selected directory and all files in this directory.
	 *
	 * @param string $sPath: Full path to directory
	 * @param bool $bResursive: Delete if dir not empty
	 * @param bool $bDeleteSourceDir: Delete current directory or all content
	 */
	private static function _rmDir($sPath, $bResursive=false, $bDeleteSourceDir=true) {
	    $sPath = rtrim($sPath, '/') . '/';
	    $oHandle = opendir($sPath);
	    while (false !== ($sFile = readdir($oHandle))) {
	        if($sFile == "." OR $sFile == ".." )
	        	continue;
	        // set fullpath to file
            $sFullpath = $sPath . $sFile;
            if (is_dir($sFullpath))
                self::_rmDir($sFullpath, $bResursive, true);
            else
                unlink($sFullpath);
	    }
	    // close current dir
	    closedir($oHandle);
	    // delete current dir
	    if ($bDeleteSourceDir)
	    	rmdir($sPath);
	}
}


/**
 * Templum exception.
 *
 * @link http://templum.googlecode.com
 * @license Apache License 2.0 http://www.apache.org/licenses/LICENSE-2.0
 * @author Anton Danilchenko <anton.danilchenko@gmail.com>
 */
class Templum_Exception extends Exception {
}